<?php
declare (strict_types = 1);

namespace app\command;

use app\common\CommandExt;
use app\common\JsonException;
use think\console\Input;
use think\console\input\Argument;
use think\console\Output;

class ServerWinform extends CommandExt
{
    protected $address;
    protected $server;
    protected $pid_file;
    protected $log_file;
    protected $delimiter;

    protected function configure()
    {
        $protocol = empty(config('app.server_winform.enable_ssl')) ? 'tcp' : 'tcp(ssl)';
        $host = config('app.server_winform.host');
        $port = config('app.server_winform.port');
        $this->address = "<{$protocol}://{$host}:{$port}>";

        // 指令配置
        $this->setName('server:winform')
            ->addArgument('action', Argument::OPTIONAL, "start|stop|reload|restart|status", 'start')
            ->setDescription('桌面程序服务端 ' . $this->address);

        $this->log_file = runtime_path() . 'log' . DIRECTORY_SEPARATOR . 'winform.log';
        // pid文件不要放到runtime目录，否则容易被清除掉，造成进程无法正常关闭
        $this->pid_file = root_path() . 'extend' . DIRECTORY_SEPARATOR . 'marks' . DIRECTORY_SEPARATOR . 'winform.pid';

        $this->delimiter = chr(0x03) . chr(0x02);
    }

    protected function execute(Input $input, Output $output)
    {
        if (!extension_loaded('swoole')) {
            $output->error('Can\'t detect Swoole extension installed.');
            return;
        }
        $action = $input->getArgument('action');
        if (in_array($action, ['start', 'stop', 'reload', 'restart', 'status'])) {
            $this->$action();
        } else {
            $output->error("Invalid argument action:{$action}, Expected start|stop|reload|restart|status.");
            return;
        }
    }

    protected function checkFile($file)
    {
        $dir = dirname($file);
        if (!is_dir($dir)) {
            @mkdir($dir, 0700, true);
        }
        @touch($file);
        if (!is_writable($file)) {
            return false;
        }
        return true;
    }

    protected function escape($data, string $characters = null, string $esc = null)
    {
        $esc = chr(0x1b);
        if (empty($characters)) {
            $characters = chr(0x03) . chr(0x02);
        }
        if (empty($esc)) {

            $esc = chr(0x1b);
        }
        $data = str_replace($esc, $esc . $esc, $data);
        foreach (str_split($characters) as $c) {
            $data = str_replace($c, $esc . $c, $data);
        }
        return $data;
    }

    protected function unescape($data, string $characters = null, string $esc = null)
    {
        if (empty($characters)) {
            $characters = chr(0x03) . chr(0x02);
        }
        if (empty($esc)) {

            $esc = chr(0x1b);
        }
        foreach (str_split($characters) as $c) {
            $data = str_replace($esc . $c, $c, $data);
        }
        $data = str_replace($esc . $esc, $esc, $data);
        return $data;
    }

    protected function mapInit()
    {
        $prefix = config('redis.default.prefix');
        $redis = redis_handle();
        $keys = $redis->keys("{$prefix}:server_winform:*");
        foreach ($keys as $key) {
            $redis->del($key);
        }
        $redis->close();
    }

    protected function mapSave($fd, $uid)
    {
        $prefix = config('redis.default.prefix');
        $redis = redis_handle();
        if (!$redis->sIsMember("{$prefix}:server_winform:u2fd:{$uid}", $fd)) {
            $redis->sAdd("{$prefix}:server_winform:u2fd:{$uid}", $fd);
        }
        $redis->set("{$prefix}:server_winform:fd2u:{$fd}", $uid);
        $redis->close();
    }

    protected function mapGetFds($uid)
    {
        $prefix = config('redis.default.prefix');
        $redis = redis_handle();
        $fds = $redis->sMembers("{$prefix}:server_winform:u2fd:{$uid}");
        $redis->close();
        return $fds;
    }

    protected function mapGetUser($fd)
    {
        $prefix = config('redis.default.prefix');
        $redis = redis_handle();
        $uid = $redis->get("{$prefix}:server_winform:fd2u:{$fd}");
        $redis->close();
        return $uid;
    }

    protected function mapDelete($fd)
    {
        $prefix = config('redis.default.prefix');
        $redis = redis_handle();
        if ($uid = $redis->get("{$prefix}:server_winform:fd2u:{$fd}")) {
            $redis->sRem("{$prefix}:server_winform:u2fd:{$uid}", $fd);
        }
        $redis->del("{$prefix}:server_winform:fd2u:{$fd}");
        $redis->close();
    }

    protected function sendData($fd, $data)
    {
        $this->server->send($fd, $this->escape($data, $this->delimiter) . $this->delimiter);
    }

    protected function pushMsg($fd, $action, $data = null)
    {
        $result = [
            'type' => 1,
            'action' => $action,
            'reqid' => '',
            'code' => 0,
            'msg' => '',
            'data' => $data,
        ];
        $this->sendData($fd, json_encode($result, JSON_UNESCAPED_UNICODE));
    }

    protected function responseMsg($fd, $action, $reqid, $code = 1, $msg = null, $data = null)
    {
        if (is_null($msg)) {
            $msg = lang("ERROR_CLIENT_RESP_{$code}");
        }
        $result = [
            'type' => 0,
            'action' => $action,
            'reqid' => $reqid,
            'code' => (int) $code,
            'msg' => $msg,
            'data' => $data,
        ];
        $this->sendData($fd, json_encode($result, JSON_UNESCAPED_UNICODE));
    }

    protected function saveLogin($fd, $uid)
    {
        $fds = $this->mapGetFds($uid);
        if (!empty($fds)) {
            $time = date("H:i");
            foreach ($fds as $v) {
                $v = (int) $v;
                $this->mapDelete($v);
                if ($v != $fd) {
                    $fd_info = $this->server->getClientInfo($v);
                    $msgData = [
                        'time' => $time,
                        'ip' => empty($fd_info['remote_ip']) ? '0.0.0.0' : $fd_info['remote_ip'],
                    ];
                    $this->pushMsg($v, "offline", $msgData);
                }
            }
        }
        $this->mapSave($fd, $uid);
    }

    protected function processPush($message)
    {
        var_dump('Push:', $message);
        $data = json_decode($message, true);
        if (json_last_error() !== JSON_ERROR_NONE) {
            return;
        }
        if (!isset($data['clientInfo']) || !isset($data['msgAction']) || !isset($data['msgData'])) {
            return;
        }
        $clientInfo = $data['clientInfo'];
        $msgAction = $data['msgAction'];
        $msgData = $data['msgData'];
        if (!empty($clientInfo['fd'])) {
            $fd = (int) $clientInfo['fd'];
            if ($this->server->exist($fd)) {
                //钩出授权消息进行处理
                if ($msgAction == 'authSuccess') {
                    $this->saveLogin($fd, $msgData['memberlogin_id']);
                    unset($msgData['memberlogin_id']);
                }
                $this->pushMsg($fd, $msgAction, $msgData);
            }
        }
        if (!empty($clientInfo['uid'])) {
            $uid = $clientInfo['uid'];
            $uids = is_scalar($uid) ? [(int) $uid] : $uid;
            if (is_array($uids)) {
                $fds = [];
                foreach ($uids as $u) {
                    $fds = array_merge($fds, $this->mapGetFds($u));
                }
                $fds = array_unique($fds);
                foreach ($fds as $fd) {
                    $fd = (int) $fd;
                    if ($this->server->exist($fd)) {
                        $this->pushMsg($fd, $msgAction, $msgData);
                    }
                }
            }
        }
    }

    protected function processRequest($fd, $data)
    {
        var_dump('Request:', $fd, $data);
        $dataObj = null;
        try
        {
            $dataObj = json_decode($data, true);
            if (json_last_error() !== JSON_ERROR_NONE) {
                failure(10000);
            }
            //参数校验
            if (empty($dataObj['reqid'])) {
                failure(10001);
            }
            if (empty($dataObj['action'])) {
                failure(10002);
            }
            //判断权限
            $exception = ['getBaseInfo', 'refreshQrcode', 'sendCaptcha', 'usernameLogin', 'emailLogin', 'mobileLogin', 'smsLogin'];
            if (!in_array($dataObj['action'], $exception) && empty($this->mapGetUser($fd))) {
                failure(10003);
            }
            //处理动作
            $func = "action_" . $dataObj['action'];
            if (method_exists($this, $func)) {
                $this->$func($fd, $dataObj);
                success();
            }
            failure(10004);
        } catch (JsonException $e) {
            $reqid = '';
            if (isset($dataObj['reqid'])) {
                $reqid = $dataObj['reqid'];
            }
            $action = 'RESPONSE';
            if (isset($dataObj['action'])) {
                $action = $dataObj['action'];
            }
            $json = $e->getJson();
            $this->responseMsg($fd, $action, $reqid, $json['code'], $json['msg'], $json['data']);
        }
    }

    protected function action_getBaseInfo($fd, $data)
    {
        $areacode = [
            [
                'code' => '86',
                'name' => '中国（+86）',
            ], [
                'code' => '86',
                'name' => '中国内地（+86）',
            ], [
                'code' => '852',
                'name' => '中国香港（+852）',
            ], [
                'code' => '853',
                'name' => '中国澳门（+853）',
            ], [
                'code' => '886',
                'name' => '中国台湾（+886）',
            ],
        ];
        success([
            'areacode' => $areacode,
            'link_register' => '/',
            'link_password' => '/',
            'link_agreement' => 'http://www.wpfly.cn/',
            'link_login' => 'http://www.wpfly.cn/member/login',
        ]);
    }

    protected function action_refreshQrcode($fd, $data)
    {
        $fd_info = $this->server->getClientInfo($fd);
        $flag = rand_str(4);
        $info = [
            'flag' => $flag,
            'fd' => $fd,
            'ip' => empty($fd_info['remote_ip']) ? '0.0.0.0' : $fd_info['remote_ip'],
        ];
        $udid = sign_data($info, '', false);
        $cacheId = "cache_qrcode_info_winform_{$udid}";
        cache($cacheId, $info, 300);
        $url = site_url('wechat/member_auth.html?udid=' . $udid);
        success([
            'flag' => $flag,
            'qrcode' => bin2hex($url),
        ]);
    }

    protected function action_sendCaptcha($fd, $data)
    {
        $this->validate($data, [
            'full_mobile' => 'require|fullMobile',
        ]);
        [$areacode, $mobile] = explode('-', $data['full_mobile']);
        $cacheId = "cache_captcha_winform_{$areacode}_{$mobile}";
        $cacheData = cache($cacheId);
        if (!empty($cacheData['last_time']) && time() < $cacheData['last_time'] + 60) {
            failure(1, "请一分钟后再获取！");
        }
        $captcha = rand_str(4);
        $info = [
            'last_time' => time(),
            'captcha' => $captcha,
        ];
        cache($cacheId, $info, 300);
        success([
            'captcha' => $captcha,
        ]);
    }

    protected function action_usernameLogin($fd, $data)
    {
        $this->validate($data, [
            'username' => 'require|username',
            'password' => 'require|password',
        ]);
        $userInfo = $this->app->db->name('member_login')->getInfo([
            ['username', '=', $data['username']],
            ['deleted', '=', 0],
        ]);
        if (empty($userInfo) || empty($userInfo['password']) || !password_verify($data['password'], $userInfo['password'])) {
            failure(1, "账户名或密码错误！");
        }
        if (empty($userInfo['enabled']) || empty($userInfo['memberrole_id'])) {
            failure(1, "账号异常！");
        }
        $this->saveLogin($fd, $userInfo['id']);
        success('ok');
    }

    protected function action_emailLogin($fd, $data)
    {
        $this->validate($data, [
            'email' => 'require|email',
            'password' => 'require|password',
        ]);
        $userInfo = $this->app->db->name('member_login')->getInfo([
            ['email', '=', $data['email']],
            ['deleted', '=', 0],
        ]);
        if (empty($userInfo) || empty($userInfo['password']) || !password_verify($data['password'], $userInfo['password'])) {
            failure(1, "账户名或密码错误！");
        }
        if (empty($userInfo['enabled']) || empty($userInfo['memberrole_id'])) {
            failure(1, "账号异常！");
        }
        $this->saveLogin($fd, $userInfo['id']);
        success('ok');
    }

    protected function action_mobileLogin($fd, $data)
    {
        $this->validate($data, [
            'full_mobile' => 'require|fullMobile',
            'password' => 'require|password',
        ]);
        [$areacode, $mobile] = explode('-', $data['full_mobile']);
        $userInfo = $this->app->db->name('member_login')->getInfo([
            ['mobile', '=', $mobile],
            ['areacode', '=', $areacode],
            ['deleted', '=', 0],
        ]);
        if (empty($userInfo) || empty($userInfo['password']) || !password_verify($data['password'], $userInfo['password'])) {
            failure(1, "账户名或密码错误！");
        }
        if (empty($userInfo['enabled']) || empty($userInfo['memberrole_id'])) {
            failure(1, "账号异常！");
        }
        $this->saveLogin($fd, $userInfo['id']);
        success('ok');
    }

    protected function action_smsLogin($fd, $data)
    {
        $this->validate($data, [
            'full_mobile' => 'require|fullMobile',
            'captcha' => 'require|number|length:4',
        ]);
        [$areacode, $mobile] = explode('-', $data['full_mobile']);
        $cacheId = "cache_captcha_winform_{$areacode}_{$mobile}";
        $cacheData = cache($cacheId);
        if (empty($cacheData['captcha']) || $cacheData['captcha'] != $data['captcha']) {
            failure(1, "验证码错误！");
        }
        $userInfo = $this->app->db->name('member_login')->getInfo([
            ['mobile', '=', $mobile],
            ['areacode', '=', $areacode],
            ['deleted', '=', 0],
        ]);
        if (empty($userInfo)) {
            $memberId = $this->app->db->name('member_login')->insertGetId([
                'memberrole_id' => '1',
                'areacode' => $areacode,
                'mobile' => $mobile,
                'enabled' => '1',
                'deleted' => '0',
            ]);
            $userInfo = $this->app->db->name('member_login')->getInfo($memberId);
        }
        if (empty($userInfo['enabled']) || empty($userInfo['memberrole_id'])) {
            failure(1, "账号异常！");
        }
        cache($cacheId, null);
        $this->saveLogin($fd, $userInfo['id']);
        success('ok');
    }

    protected function action_logout($fd, $data)
    {
        $this->mapDelete($fd);
        success('ok');
    }

    protected function action_getMemberInfo($fd, $data)
    {
        $uid = $this->mapGetUser($fd);
        $userInfo = $this->app->db->name('member_login')->field('id,username,areacode,mobile,email,nickname,avatar,lastlogintime,lastloginip,lastloginlocation')->getInfo($uid);
        success($userInfo);
    }

    protected function action_getMemberToken($fd, $data)
    {
        $uid = $this->mapGetUser($fd);
        $info = [
            'uid' => $uid,
        ];
        $token = sign_data($info, '', false);
        $cacheId = "cache_member_login_winform_{$token}";
        cache($cacheId, $info, 30);
        success($token);
    }

    protected function start()
    {
        if ($this->isRunning()) {
            $this->output->error('Swoole server is already running.');
            return;
        }
        $this->output->writeln('Starting swoole server...');

        if (!$this->checkFile($this->pid_file)) {
            $this->output->error("Can\'t write file: {$this->pid_file}.");
            return;
        }
        if (!$this->checkFile($this->log_file)) {
            $this->output->error("Can\'t write file: {$this->log_file}.");
            return;
        }

        $this->mapInit();

        $sockType = SWOOLE_SOCK_TCP;
        $option = array(
            'worker_num' => 2,
            'pid_file' => $this->pid_file,
            'log_file' => $this->log_file,
            'daemonize' => true,
            'open_eof_check' => true,
            'package_eof' => $this->delimiter,
            'heartbeat_check_interval' => 6,
            'heartbeat_idle_time' => 30,
        );
        if (!empty(config('app.server_winform.enable_ssl'))) {
            $sockType = $sockType | SWOOLE_SSL;
            $option['ssl_cert_file'] = config('app.server_winform.cert_file');
            $option['ssl_key_file'] = config('app.server_winform.key_file');
        }
        $this->server = new \swoole_server(config('app.server_winform.host'), (int) config('app.server_winform.port'), SWOOLE_PROCESS, $sockType);
        $this->server->set($option);

        $this->server->on('Connect', function ($server, $fd, $reactorId) {
            var_dump("Connect:$fd");
        });

        // 单独的进程里订阅redis消息并推送到客户端
        $this->server->addProcess(new \swoole_process(function () {
            ini_set('default_socket_timeout', '-1');
            $redis = redis_handle(0, 'default', true);
            $redis->subscribe(['msg_server_winform'], function ($instance, $channelName, $message) {
                $this->processPush($message);
            });
            $redis->close();
        }));

        $this->server->on('Receive', function ($server, $fd, $reactorId, $data) {
            var_dump("Receive:" . bin2hex($data));
            foreach (explode($this->delimiter, $data) as $s) {
                if ($s === '') {
                    continue;
                }
                $this->processRequest($fd, $this->unescape($s, $this->delimiter));
            }
        });

        $this->server->on('Close', function ($server, $fd) {
            var_dump("Close:$fd");
            $this->mapDelete($fd);
        });

        $this->output->writeln("Swoole server started: {$this->address}");

        $this->server->start();
    }

    protected function stop()
    {
        if (!$this->isRunning()) {
            $this->output->error('No swoole server running.');
            return;
        }
        $this->output->writeln('Stopping swoole server...');
        $pid = (int) file_get_contents($this->pid_file);
        \swoole_process::kill($pid, SIGTERM);
        $end = time() + 15;
        while (time() < $end && \swoole_process::kill($pid, 0)) {
            usleep(100000);
        }
        if ($this->isRunning()) {
            $this->output->error('Unable to stop the swoole server.');
            return;
        }
        $this->output->writeln('Stopping success.');
    }

    protected function reload()
    {
        if (!$this->isRunning()) {
            $this->output->error('No swoole server running.');
            return;
        }
        $this->output->writeln('Reloading swoole server...');
        $pid = (int) file_get_contents($this->pid_file);
        if (!\swoole_process::kill($pid, SIGUSR1)) {
            $this->output->error('Reloading failure.');
            return;
        }
        $this->output->writeln('Reloading success.');
    }

    protected function restart()
    {
        if ($this->isRunning()) {
            $this->stop();
        }
        $this->start();
    }

    protected function status()
    {
        $this->output->writeln($this->isRunning() ? 'Server is running.' : 'Server stopped.');
    }

    protected function isRunning()
    {
        if (!is_readable($this->pid_file)) {
            return false;
        }
        $pid = (int) file_get_contents($this->pid_file);
        return $pid > 0 && \swoole_process::kill($pid, 0);
    }
}
